/**
 * @file proto.c
 * Implementation of the P2P3TC functionality.
 */

#include "proto.h"
#include "common.h"
#include "net.h"
#include "file.h"

#include <stdlib.h>
#include <string.h>

void proto_recv(Node *node, const Msg *msg)
{
	/* Reply message. DON'T return from inside the switch, otherwise the message doesn't get deleted at the end. */
	Msg *reply = msg_new();
	switch(msg->type) {
	case P2P_MSG_JOIN_REQ: {
		msg_set_hdr(reply, P2P_VERSION, P2P_MSG_JOIN_ACK, 1, node->addr, &msg->src);
		msg_set_join_ack(reply, node->addr, node->addr_right);
		msg_write_tcp(node, reply, node->sock_tcp_conn);
		break;
	}
	case P2P_MSG_JOIN_ACK: {
		verbose(node, VPROTO, "Received unsolicited P2P_JOIN_ACK. Discarded.\n");
		break;
	}
	case P2P_MSG_LINK_UPDATE: {
		addr_cpy(msg->d.link_up.side == LEFT? node->addr_left : node->addr_right,
			&msg->d.link_up.neighbour);
		break;
	}
	case P2P_MSG_SEARCH: {
		msg_rebroadcast(node, msg);
		uint32_t file_size;
		if(file_is_available(node, msg->d.search.file_name, &file_size) == P2P_ERROR)
			break;
		msg_set_hdr(reply, P2P_VERSION, P2P_MSG_REPLY, 1, node->addr, &msg->d.search.bcast_init);
		msg_set_reply(reply, msg->d.search.req_id, file_size);
		msg_send_udp(node, reply);
		break;
	}
	case P2P_MSG_REPLY: {
		if(msg->d.reply.req_id != node->req_id)
			verbose(node, VPROTO, "Received P2P_REPLY to an old search (req_id=%d). Discarded.\n", msg->d.reply.req_id);
		else search_add(node->search, &msg->src, msg->d.reply.file_size);
		break;
	}
	case P2P_MSG_NEIGHBORS_REQ: {
		msg_rebroadcast(node, msg);
		msg_set_hdr(reply, P2P_VERSION, P2P_MSG_NEIGHBORS_LIST, 1,
			node->addr, &msg->d.neighbours_req.bcast_init);
		msg_set_neighbours_list(reply, node->addr_left, node->addr_right, node->srv_name);
		msg_send_udp(node, reply);
		break;
	}
	case P2P_MSG_NEIGHBORS_LIST: {
		break;
	}
	case P2P_MSG_GET: {
		uint32_t status;
		uint32_t value;
		uint32_t begin = msg->d.get.begin;
		uint32_t end = msg->d.get.end;
		if(file_is_available(node, msg->d.get.file_name, &value) == P2P_ERROR)
			status = P2P_DATA_ERROR;
		else if(begin < 0 || end < begin || end > value ||
			end-begin+1 > P2P_MSG_MAX_DATA_SIZE-8)
			status = P2P_DATA_ERROR, value = P2P_BAD_REQUEST;
		else status = P2P_DATA_OK, value = end-begin+1;
		msg_set_hdr(reply, P2P_VERSION, P2P_MSG_DATA, 1, node->addr, &msg->src);
		msg_set_data(reply, status, value);
		file_get_chunk(node, msg->d.get.file_name, begin, end, reply->d.data.data);
		msg_write_tcp(node, reply, node->sock_tcp_conn);
		break;
	}
	case P2P_MSG_DATA: {
		verbose(node, VPROTO, "Received unsolicited P2P_DATA. Discarded.\n");
		break;
	}
	default:
		verbose(node, VPROTO, "Received unknown message. Discarded.\n");
		break;
	}
	msg_delete(&reply);
}

/** 
 * 
 * Handles TCP connection on its own.
 */
int proto_send_link_up(Node *node, const Addr *dst, const Addr *neighbour, uint32_t side)
{
	verbose(node, VPROTO, "Sending P2P_LINK_UPDATE\n");
	/* Build the message P2P_LINK_UPDATE */
	Msg *msg = msg_new();
	msg_set_hdr(msg, P2P_VERSION, P2P_MSG_LINK_UPDATE, 1, node->addr, dst);
	msg_set_link_up(msg, neighbour, side);
	/* Send the message */
	int ret = msg_send_tcp(node, msg);
	msg_delete(&msg);
	return ret;
}

int proto_join(Node *node, const Addr *dst)
{
	//FIXME proto_leave() before join
	//FIXME try to do an atomic join
	verbose(node, VPROTO, "Initiating JOIN\n");
	
	/* Open connection to dst */
	int sock;
	if((sock = tcp_socket_create(node, dst)) == P2P_ERROR) {
		verbose(node, VPROTO, "JOIN failed.\n");
		return P2P_ERROR;
	}
	/* Send P2P_JOIN_REQ */
	Msg *msg = msg_new();
	msg_set_hdr(msg, P2P_VERSION, P2P_MSG_JOIN_REQ, 1, node->addr, dst);
	if(msg_write_tcp(node, msg, sock) == P2P_ERROR) {
		msg_delete(&msg);
		socket_close(node, sock);
		return P2P_ERROR;
	}
	/* Receive P2P_JOIN_ACK */
	if(msg_read_tcp(node, msg, sock) == P2P_ERROR) {
		msg_delete(&msg);
		socket_close(node, sock);
		return P2P_ERROR;
	}
	/* Close connection to dst */
	socket_close(node, sock);
	if(msg->type != P2P_MSG_JOIN_ACK) {
		verbose(node, VPROTO, "Error! Expecting P2P_JOIN_ACK. Received %d.\n", msg->type);
		msg_delete(&msg);
		return P2P_ERROR;
	}
	/* Update neighbours */
	addr_cpy(node->addr_left, &msg->d.join_ack.left);
	addr_cpy(node->addr_right, &msg->d.join_ack.right);
	msg_delete(&msg); // we're done with msg
	/* Update topology */
	if(proto_send_link_up(node, node->addr_left, node->addr, RIGHT) == P2P_ERROR
	|| proto_send_link_up(node, node->addr_right, node->addr, LEFT) == P2P_ERROR)
		return P2P_ERROR;

	return P2P_OK;
}

void proto_leave(Node *node)
{
	if(addr_cmp(node->addr, node->addr_left) != 0
	&& addr_cmp(node->addr, node->addr_right) != 0) {
		proto_send_link_up(node, node->addr_left, node->addr_right, RIGHT);
		proto_send_link_up(node, node->addr_right, node->addr_left, LEFT);
	} else verbose(node, VDEBUG, "proto_leave(): not connected to a P2P3TC network\n");
	/* Update our state anyway, just in case other malfunctioning node messed things up */
	addr_cpy(node->addr_left, node->addr);
	addr_cpy(node->addr_right, node->addr);
}

int proto_get(Node *node, Addr *peer, char *file_name, uint32_t file_size)
{
	verbose(node, VDEBUG, "GET _%s_ (%d bytes) from %s\n", file_name, file_size, addr_get_str(peer));

	int r; // total bytes received so far
	int size = P2P_MSG_MAX_DATA_SIZE-8; // fragment size
	int sock;
	int ret = P2P_OK;

	Msg *request = msg_new();
	Msg *reply = msg_new();
	msg_set_hdr(request, P2P_VERSION, P2P_MSG_GET, 1, node->addr, peer);
	msg_set_get(request, 0, 0, file_name);

	for(r = 0; r < file_size && ret == P2P_OK; r += size) {
		size = min(size, file_size-r);
		request->d.get.begin = r;
		request->d.get.end = r+size-1;
		/* Open connection to peer */
		if((sock = tcp_socket_create(node, peer)) == -1)
			ret = P2P_ERROR;
		/* Send P2P_GET */
		if(ret == P2P_OK
		&& msg_write_tcp(node, request, sock) == P2P_ERROR)
			ret = P2P_ERROR;
		/* Receive P2P_DATA */
		if(ret == P2P_OK
		&& msg_read_tcp(node, reply, sock) == P2P_ERROR)
			ret = P2P_ERROR;
		if(ret == P2P_OK && reply->type != P2P_MSG_DATA) {
			verbose(node, VPROTO, "Error! Expecting P2P_DATA. Received %d.\n", reply->type);
			ret = P2P_ERROR;
		}
		if(ret == P2P_OK && reply->d.data.status != P2P_DATA_OK) {
			verbose(node, VPROTO, "Error! status=%d, value=%d\n",
				reply->d.data.status, reply->d.data.value);
			ret = P2P_ERROR;
		}
		/* Close connection to peer */
		socket_close(node, sock);
		/* Write data to file */
		if(ret == P2P_OK
		&& file_set_chunk(node, file_name, request->d.get.begin,
		request->d.get.end, reply->d.data.data) == P2P_ERROR)
			ret = P2P_ERROR;
	}

	msg_delete(&request);
	msg_delete(&reply);
	verbose(node, VDEBUG, "GET %s\n", ret == P2P_OK? "done" : "failed");

	return ret;
}

int proto_search(Node *node, char *file_name)
{
	verbose(node, VDEBUG, "Starting SEARCH\n");
	/* Start a new search */
	search_clear(node->search);
	search_set(node->search, ++node->req_id, file_name);
	/* Build the message P2P_SEARCH */
	Msg *msg = msg_new();
	msg_set_hdr(msg, P2P_VERSION, P2P_MSG_SEARCH, P2P_MSG_TTL_DFL, node->addr, addr_bcast());
	msg_set_search(msg, node->addr, node->req_id, file_name);
	/* Broadcast the message */
	int sock;
	int ret = P2P_OK;
	/* 	Send to left neighbour */
	if((sock = udp_socket_create(node, node->addr_left)) == -1) {
		ret = P2P_ERROR;
		goto proto_search_finally;
	}
	if(msg_write_udp(node, msg, sock) == P2P_ERROR) {
		ret = P2P_ERROR;
		goto proto_search_finally;
	}
	socket_close(node, sock);
	/* 	Send to right neighbour */
	if((sock = udp_socket_create(node, node->addr_right)) == -1) {
		ret = P2P_ERROR;
		goto proto_search_finally;
	}
	if(msg_write_udp(node, msg, sock) == P2P_ERROR) {
		ret = P2P_ERROR;
		goto proto_search_finally;
	}
proto_search_finally:
	socket_close(node, sock);
	msg_delete(&msg);
	return ret;
}
